package org.zxteam.virtualfs.org.zxteam.virtualfs.ssh;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import org.apache.commons.collections.primitives.ArrayByteList;
import org.zxteam.virtualfs.IFileSystem;

import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;

import static org.apache.commons.io.IOUtils.toByteArray;

public class SshFileSystem implements IFileSystem, Closeable {

    private final Session session;

    public SshFileSystem(final Session sshSession) {
        if (sshSession == null) {
            throw new IllegalArgumentException();
        }
        this.session = sshSession;
    }

    public void close() throws IOException { }

    @Override
    public Iterable<String> list(final String path) throws JSchException, IOException {
        if (path == null) {
            throw new IllegalArgumentException();
        }
        String[] list = getStreamOutput(path, "ls").split("\\n");
        return new ArrayList<String>(Arrays.asList(list));
    }

    @Override
    public InputStream openInputStream(final String path) throws JSchException, IOException {
        if (path == null) {
            throw new IllegalArgumentException();
        }
        return new SCPInputStream(session, path);
    }

    public OutputStream openOutputStream(final String path) throws JSchException, IOException {
        if (path == null) {
            throw new IllegalArgumentException();
        }
        return new SCPOutputStream(session, path);
    }

    @Override
    public byte[] getHashSHA1(final String path) throws IOException, JSchException {
        return getCheckSum(path, "sha1sum");
    }

    @Override
    public byte[] getHashSHA512(final String path) throws IOException, JSchException {
        return getCheckSum(path, "sha512sum");
    }

    @Override
    public byte[] getHashMD5(final String path) throws IOException, JSchException {
        return getCheckSum(path, "md5sum");
    }

    private byte[] getCheckSum(final String path, final String cmd) throws JSchException, IOException {
        String[] list = getStreamOutput(path, cmd).split(" ");
        return list[0].getBytes();
    }

    private String getStreamOutput(final String path, final String cmd) throws JSchException, IOException {
        if (path == null || cmd == null) {
            throw new IllegalArgumentException();
        }

        Channel channel = session .openChannel("exec");
        ((ChannelExec)channel).setCommand(cmd + " " + path);
        channel.connect();
        InputStream is = channel.getInputStream();
        byte[] out = toByteArray(is);
        String buffer = new String(out, "UTF-8");

        if (channel.getExitStatus() == 2) {
            throw new IOException("Error execute " + cmd + " " + path);
        }

        is.close();
        channel.disconnect();
        return buffer;
    }

    private class SCPInputStream extends InputStream {

        private final Channel channel;

        private final OutputStream outputStream;

        private final InputStream inputStream;

        private ArrayByteList buffer;

        private InputStream contentInputStream;

        public SCPInputStream(final Session session, final String path) throws IOException, JSchException {
            channel = session.openChannel("exec");
            ((ChannelExec)channel).setCommand("scp -f " + path);
            channel.connect();

            this.outputStream = channel.getOutputStream();
            this.inputStream = channel.getInputStream();
            this.buffer = new ArrayByteList();
            readFile();
        }

        @Override
        public int read() throws IOException {
            if (contentInputStream != null) {
                return contentInputStream.read();
            } else {
                return -1;
            }
        }

        @Override
        public void close() throws IOException {
            inputStream.close();
            outputStream.close();
            channel.disconnect();
            super.close();
        }

        private void readFile() throws IOException {
            byte[] buf = new byte[1024];

            // send '\0' for receiving header (file size, file name)
            buf[0] = 0; outputStream.write(buf, 0, 1); outputStream.flush();

            while(true) {
                int c = checkStreaming();
                if (c != 'C') {
                    break;
                }

                // read permissions
                inputStream.read(buf, 0, 5);

                long fileSize = 0L;
                while (true) {
                    if (inputStream.read(buf, 0, 1) < 0) {
                        // error
                        break;
                    }
                    if (buf[0] == ' ') break;
                    fileSize = fileSize * 10L + (long) (buf[0] - '0');
                }

                String fileName;
                for (int i = 0; ; i++) {
                    inputStream.read(buf, i, 1);
                    if (buf[i] == (byte) 0x0a) {
                        fileName = new String(buf, 0, i);
                        break;
                    }
                }

                //System.out.println(fileName + "(" + fileSize + ")");

                // send '\0' for receiving content
                buf[0] = 0;
                outputStream.write(buf, 0, 1);
                outputStream.flush();

                // read a content of file
                int contentSize;
                while(true){
                    if(buf.length < fileSize) {
                        contentSize = buf.length;
                    } else {
                        contentSize = (int)fileSize;
                    }
                    contentSize = inputStream.read(buf, 0, contentSize);
                    if(contentSize < 0){
                        // error
                        throw new IOException("Error reading content of file");
                    }
                    byte[] bytes = Arrays.copyOfRange(buf, 0, contentSize);
                    for (byte b: bytes) {
                        this.buffer.add(b);
                    }

                    fileSize -= contentSize;
                    if(fileSize == 0L) break;
                }

                // send '\0'
                buf[0] = 0;
                outputStream.write(buf, 0, 1);
                outputStream.flush();
            }

            if (buffer != null) {
                contentInputStream = new ByteArrayInputStream(buffer.toArray());
            }
        }

        private int checkStreaming() throws IOException {
            int b = inputStream.read();
            if (b == 0) return b;
            if (b == -1) return b;

            if(b == 1 || b == 2){
                StringBuilder sb = new StringBuilder();
                int c;
                do {
                    c = inputStream.read();
                    sb.append((char)c);
                }
                while(c != '\n');
                throw new IOException(sb.toString());
            }
            return b;
        }
    }

    private class SCPOutputStream extends OutputStream {

        private static final String PERMISSIONS = "C0644";

        private ArrayByteList buffer;

        private final OutputStream outputStream;

        private final InputStream inputStream;

        private final Channel channel;

        private final String path;

        public SCPOutputStream(final Session session, final String path) throws JSchException, IOException {
            channel = session.openChannel("exec");
            ((ChannelExec)channel).setCommand("scp -t " + path);
            channel.connect();

            this.outputStream = channel.getOutputStream();
            this.inputStream = channel.getInputStream();
            this.path = path;
            this.buffer = new ArrayByteList();
        }

        @Override
        public void write(int b) {
            if (buffer != null) {
                buffer.add((byte) b);
            }
        }

        @Override
        public void write(byte[] bytes) {
            if (bytes == null) {
                throw new IllegalArgumentException();
            }

            if (buffer != null) {
                for (byte b : bytes) {
                    this.buffer.add(b);
                }
            }
        }

        @Override
        public void close() throws IOException {
            write();
            outputStream.close();
            inputStream.close();
            channel.disconnect();
            super.close();
        }

        private void write() throws IOException {
            if (buffer == null) {
                throw new UnsupportedOperationException("No data to writing");
            }

            // Send file data
            String fileData = PERMISSIONS + " " + buffer.size() + " " + new File(path).getName() + "\n";
            writeBytesToStream(fileData.getBytes());

            // Send file content
            writeBytesToStream(buffer.toArray());

            // Send end of file
            writeBytesToStream("\0".getBytes());
        }

        private void writeBytesToStream(byte[] bytes) throws IOException {
            outputStream.write(bytes);
            outputStream.flush();
            if(checkStreaming() != 0){
                throw new IOException("Error with writing remote file");
            }
        }

        private int checkStreaming() throws IOException {
            int b = inputStream.read();
            if (b == 0) {
                return b;
            } else {
                int c;
                do {
                    c = inputStream.read();
                }
                while (c != '\n');
            }
            return b;
        }
    }
}
